package ch.sv7.tool.dbmanager.db;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ch.sv7.tool.dbmanager.StringUtil;
import ch.sv7.tool.dbmanager.db.difference.ColumnAutoIncrementDifference;
import ch.sv7.tool.dbmanager.db.difference.ColumnDefaultValueDifference;
import ch.sv7.tool.dbmanager.db.difference.ColumnJDBCTypeDifference;
import ch.sv7.tool.dbmanager.db.difference.ColumnSizeTooBigDifference;
import ch.sv7.tool.dbmanager.db.difference.ColumnSizeTooSmallDifference;
import ch.sv7.tool.dbmanager.db.difference.ColumnTypeDifference;
import ch.sv7.tool.dbmanager.db.difference.NotNullableColumnDifference;
import ch.sv7.tool.dbmanager.db.difference.NullableColumnDifference;
import ch.sv7.tool.dbmanager.db.difference.SchemaDifferences;

public class ColumnType implements Serializable {

	private static final long serialVersionUID = 1L;
	public static final long NO_SIZE_PROVIDED = -1L;
	private static Map typesMap;

	private int jdbcType;
	private String typeName;
	private long size;
	private long decimalSize;
	private boolean nullable;
	private boolean autoIncrement;
	private String defaultValue;

	public ColumnType(int type, long size, long decimalSize, boolean nullable, boolean autoIncrement, String typeName, String defaultValue) {
		this.jdbcType = type;
		this.size = size;
		this.decimalSize = decimalSize;
		this.nullable = nullable;
		this.typeName = typeName;
		this.autoIncrement = autoIncrement;
		this.defaultValue = defaultValue;
	}

	public ColumnType cloneType() {
		return new ColumnType(jdbcType, size, decimalSize, nullable, autoIncrement, typeName, defaultValue);
	}
	
	public boolean equalTypeName(ColumnType other) {
		if (typeName != null && other.typeName != null) {
			return typeName.equalsIgnoreCase(other.getTypeName());
		} else {
			return true;
		}
	}
	
	public void computeDifferencesFromActual(
			SchemaDifferences diffs, 
			ColumnSchema expectedColumnSchema, 
			ColumnSchema actualColumnSchema) {
		ColumnType actualType = actualColumnSchema.getType();
		List diffList = new ArrayList();
		
		if (!equalTypeName(actualType)) {
			// Actual type name
			diffList.add(new ColumnTypeDifference(expectedColumnSchema, actualColumnSchema, this, actualType));
		} 
		
		if (autoIncrement != actualType.autoIncrement){
			// Auto increment
			diffList.add(new ColumnAutoIncrementDifference(expectedColumnSchema, actualColumnSchema, this.autoIncrement, actualType.isAutoIncrement()));
		} 
		
		if (getSize() != actualType.getSize() && !(getSize() == ColumnType.NO_SIZE_PROVIDED)
				&& !(actualType.getSize() == ColumnType.NO_SIZE_PROVIDED)) {
			// Size
			long expectedSize = getSize();
			long actualSize = actualType.getSize();
			if (actualSize < expectedSize) {
				diffList.add(new ColumnSizeTooSmallDifference(expectedColumnSchema, actualColumnSchema, "size", expectedSize, actualSize));
			} else {
				diffList.add(new ColumnSizeTooBigDifference(expectedColumnSchema, actualColumnSchema, "size", expectedSize, actualSize));
			}
		} 
		
		if (isNullable() != actualType.isNullable()){
			// Nullability
			if (isNullable()){
				diffList.add(new NotNullableColumnDifference(expectedColumnSchema, actualColumnSchema));
			} else {
				diffList.add(new NullableColumnDifference(expectedColumnSchema, actualColumnSchema));
			}
		} 
		
		if (getDefaultValue() != null) {
			// Default value
			if (!getDefaultValue().equals(actualType.getDefaultValue())){
				diffList.add(new ColumnDefaultValueDifference(expectedColumnSchema, actualColumnSchema, getDefaultValue(), actualType.getDefaultValue()));
			}
		} else if (actualType.getDefaultValue() != null) {
			// Default value
			if (!actualType.getDefaultValue().equals(getDefaultValue())){
				diffList.add(new ColumnDefaultValueDifference(expectedColumnSchema, actualColumnSchema, getDefaultValue(), actualType.getDefaultValue()));
			}
		} 
		
		if (getDecimalSize() != expectedColumnSchema.getType().getDecimalSize() && !(getDecimalSize() == ColumnType.NO_SIZE_PROVIDED)
				&& !(actualType.getDecimalSize() == ColumnType.NO_SIZE_PROVIDED)) {
			// Decimal size
			long expectedSize = getDecimalSize();
			long actualSize = actualType.getDecimalSize();
			if (actualSize < expectedSize) {
				diffList.add(new ColumnSizeTooSmallDifference(expectedColumnSchema, actualColumnSchema, "decimal size", expectedSize, actualSize));
			} else {
				diffList.add(new ColumnSizeTooBigDifference(expectedColumnSchema, actualColumnSchema, "decimal size", expectedSize, actualSize));
			}
		} 
		
		if ((getTypeName() == null || actualType.getTypeName() == null) && getJDBCType() != actualType.getJDBCType()){
			// JDBC type
			diffList.add(new ColumnJDBCTypeDifference(expectedColumnSchema, actualColumnSchema, this, actualType));
		}
		
		diffs.addFirstDifferenceOfList(actualColumnSchema, diffList);
	}

	public long getSize() {
		return size;
	}

	public int getJDBCType() {
		return jdbcType;
	}

	public String getJDBCTypeName() {
		return jdbcTypeToString(jdbcType);
	}

	public void setSize(long size) {
		this.size = size;
	}

	public void setJDBCType(int type) {
		this.jdbcType = type;
	}

	public String getTypeName() {
		return typeName;
	}

	public void setTypeName(String typeName) {
		this.typeName = typeName;
	}
	
	public String getDefaultValue() {
		return defaultValue;
	}

	public void setDefaultValue(String defaultValue) {
		this.defaultValue = defaultValue;
	}

	public boolean hasDefaultValue() {
		return defaultValue != null && defaultValue.trim().length() > 0;
	}
	
	/**
	 * @return the autoIncrement
	 */
	public boolean isAutoIncrement() {
		return autoIncrement;
	}

	/**
	 * @param autoIncrement the autoIncrement to set
	 */
	public void setAutoIncrement(boolean autoIncrement) {
		this.autoIncrement = autoIncrement;
	}

	public boolean equals(Object obj) {
		if (obj == null || !(obj instanceof ColumnType))
			return false;
		ColumnType ct = (ColumnType) obj;
		return (jdbcType == ct.jdbcType) 
			&& equalTypeName(ct) 
			&& size == ct.size 
			&& decimalSize == ct.decimalSize 
			&& nullable == ct.nullable
			&& autoIncrement == ct.autoIncrement
			&& StringUtil.safeEquals(defaultValue, ct.defaultValue);
	}

	public static String jdbcTypeToString(int type) {
		Integer value = new Integer(type);
		Iterator iter = typesMap.entrySet().iterator();
		while (iter.hasNext()) {
			Map.Entry entry = (Map.Entry) iter.next();
			if (entry.getValue().equals(value)) {
				return (String) entry.getKey();
			}
		}

		return "UNKNOWN-TYPE";
	}

	public static int stringToJdbcType(String type) {
		Integer typeValue = (Integer) typesMap.get(type);
		if (typeValue == null) {
			return 0;
		} else {
			return typeValue.intValue();
		}
	}

	public static Set getAvailableJdbcTypeNameSet() {
		return typesMap.keySet();
	}
	
	public long getDecimalSize() {
		return decimalSize;
	}

	public void setDecimalSize(long decimalSize) {
		this.decimalSize = decimalSize;
	}

	public boolean isNullable() {
		return nullable;
	}

	public void setNullable(boolean nullable) {
		this.nullable = nullable;
	}

	public String toString() {
		return jdbcTypeToString(jdbcType);
	}
	
	public boolean mustBeQuoted() {
		switch (jdbcType) {
		case Types.BIT:
		case Types.LONGVARCHAR:
		case Types.CHAR:
		case Types.VARCHAR:
		case Types.DATE:
		case Types.TIME:
		case Types.TIMESTAMP:
		case Types.CLOB:
			return true;
		}
		return false;
	}

	public boolean isBinary() {
		switch (jdbcType) {
		case Types.LONGVARBINARY:
		case Types.VARBINARY:
		case Types.BINARY:
		case Types.BLOB:
			return true;
		}
		return false;
	}

	static {
		/*
		 * Getting all types from java.sql.Types by reflection
		 */
		if (typesMap == null) {
			typesMap = new HashMap();
			Field[] constants = Types.class.getDeclaredFields();
			for (int i = 0; i < constants.length; i++) {
				if (constants[i].getType().equals(int.class) && Modifier.isStatic(constants[i].getModifiers())) {
					try {
						String name = constants[i].getName();
						Integer value = (Integer) constants[i].get(null);
						typesMap.put(name, value);
					} catch (IllegalArgumentException e) {
						e.printStackTrace();
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}
}
